const cache = require('../../utils/cache')
const parser = createParser()
const Setting = require('../../tables/Setting')
const hx = require('hbuilderx')
const log = require('../../utils/log')

const { now, toTime, toUnitValue, toRatio, toDuration } = require('../../utils/common')
const { isNil } = require('lodash')

let setting

let userChartsCache = 'user-charts'
let userCharts = ['cpu_', 'mem_']
let userChartsSet

let processIntervalCache = 'process-interval'
let processInterval = 5
let processTimer

let chartIntervalCache = 'chart-interval'
let chartInterval = 0
let chartTimer

async function onInit() {
    const client = cache.connections.get(this.options.host).connection
    client.onHeartbeat = data => this.onFn('setStats', data)
    setting = new Setting(this.options.host)
    setInfo.call(this, client)
    setMetrics.call(this, client)
    setUsers.call(this)
    return {
        name: this.options.name,
        ip: this.options.host
    }
}

async function setInfo(client) {
    const stats = {
        os: -1,
        osVersion: -1,
        time: -1,
        time_ms: -1,
        upTotalTime: -1,
        upTotalFreeTime: -1,
        userTotal: -1
    }
    stats.time_ms = await client.execute('date +%s%3N')
    stats.time = toTime(Number(stats.time_ms))
    const os = await parser.os(client)
    const uptime = await parser.uptime(client)
    const users = await parser.users(client)
    stats.os = os.NAME
    stats.osVersion = os.VERSION
    stats.upTotalTime = uptime.upTotalTime
    stats.upTotalFreeTime = uptime.upTotalFreeTime
    stats.userTotal = users.length
    this.onFn('setStats', stats)
}
async function setMetrics(client) {
    let metricGroup = await parser.metrics(client)
    userCharts = await setting.get(userChartsCache, userCharts)
    chartInterval = await setting.get(chartIntervalCache, chartInterval)
    processInterval = await setting.get(processIntervalCache, processInterval)
    await this.onFn('setMetricGroup', {
        metricGroup,
        ids: userCharts,
        chartInterval,
        processInterval
    })
    changeChartInterval.call(this, chartInterval, false)
    changeProcessInterval.call(this, processInterval, false)
    client.onHeartbeat = data => {
        this.onFn('setStats', data)
        if (chartInterval == 0) {
            setALL.call(this)
        }
        if (processInterval == 0) {
            setProcess.call(this)
        }
    }
}
async function setProcess() {
    let client = cache.connections.get(this.options.host).connection
    let items = await parser.process(client)
    this.onFn('setProcess', {
        items,
        time: now('mm:ss')
    })
}
async function setUsers() {
    let client = cache.connections.get(this.options.host).connection
    let items = await parser.users(client)
    this.onFn('setUsers', {
        items
    })
}
async function setALL() {
    let set = new Set()
    let mem = false
    let cpu = false
    let hdd = false
    let net = false
    let loadAvg = false
    userCharts.forEach(it => {
        set.add(it)
        if (it.startsWith('mem_')) {
            mem = true
        } else if (it.startsWith('cpu_')) {
            cpu = true
        } else if (it.startsWith('hdd_')) {
            hdd = true
        } else if (it.startsWith('net_')) {
            net = true
        } else if (it.startsWith('load-avg_')) {
            loadAvg = true
        }
    })
    userChartsSet = set
    setCurrentLoadAvg.call(this)
    if (mem) {
        setMEM.call(this)
    }
    if (cpu) {
        setCPU.call(this)
    }
    if (hdd) {
        setHDD.call(this)
    }
    if (net) {
        setNET.call(this)
    }
    if (loadAvg) {
        setLoadAvg.call(this)
    }
}
async function setMEM() {
    let client = cache.connections.get(this.options.host).connection
    let items = await parser.mem(client)
    items.forEach(it => {
        this.onFn('appendPoint', {
            id: it.name,
            data: it
        })
    })
}
async function setCPU() {
    let client = cache.connections.get(this.options.host).connection
    let cpus = await parser.cpu(client)
    cpus.forEach(it => {
        this.onFn('appendPoint', {
            id: it.name,
            data: it
        })
    })
}
async function setHDD() {
    let client = cache.connections.get(this.options.host).connection
    let items = await parser.hdd(client)
    items.forEach(it => {
        this.onFn('appendPoint', {
            id: it.name,
            data: it
        })
    })
}
async function setNET() {
    let client = cache.connections.get(this.options.host).connection
    let items = await parser.net(client)
    items.forEach(it => {
        this.onFn('appendPoint', {
            id: it.name,
            data: it
        })
    })
}
async function setLoadAvg() {
    let client = cache.connections.get(this.options.host).connection
    let items = await parser.loadAvg(client)
    items.forEach(it => {
        this.onFn('appendPoint', {
            id: it.name,
            data: it
        })
    })
}
async function setCurrentLoadAvg() {
    let client = cache.connections.get(this.options.host).connection
    let items = await parser.loadAvg(client)
    if (items.length > 0) {
        this.onFn('setLoadAvg', items.at(0))
    }
}

function createParser() {
    let lastCPU
    let lastHDD
    let lastNET
    let sectors = new Map()

    const command = {
        CPU: 'cat /proc/stat',
        HDD: 'cat /proc/diskstats',
        MEM: 'free -m',
        NET: 'cat /proc/net/dev',
        PROCESS: 'ps aux',
        VERSION: 'cat /proc/version',
        UP_TIME: 'cat /proc/uptime',
        LOAD_AVG: 'cat /proc/loadavg',
        USERS: 'cat /etc/passwd',
        OS: 'cat /etc/os-release'
    }
    async function cpuStat(client) {
        const text = await client.execute(command.CPU)
        const time = now('mm:ss')
        const rows = text.split('\n')
        const maps = new Map()
        rows.forEach(it => {
            let match = it.match(/cpu(\d*)/)
            if (match) {
                let name = 'cpu_' + match[1]
                const cpu = {
                    name,
                    time,
                    user: 0,
                    nice: 0,
                    system: 0,
                    idle: 0,
                    iowait: 0,
                    irq: 0,
                    softirq: 0,
                    steal: 0,
                    guest: 0,
                    guest_nice: 0,
                    total: 0
                }
                let parts = it.split(/\s+/)
                if (parts.length >= 8) {
                    cpu.user = parseInt(parts[1], 10) || 0
                    cpu.nice = parseInt(parts[2], 10) || 0
                    cpu.system = parseInt(parts[3], 10) || 0
                    cpu.idle = parseInt(parts[4], 10) || 0
                    cpu.iowait = parseInt(parts[5], 10) || 0
                    cpu.irq = parseInt(parts[6], 10) || 0
                    cpu.softirq = parseInt(parts[7], 10) || 0
                    cpu.steal = parseInt(parts[8], 10) || 0
                    cpu.guest = parseInt(parts[9], 10) || 0
                    cpu.guest_nice = parseInt(parts[10], 10) || 0
                    cpu.total =
                        cpu.user +
                        cpu.nice +
                        cpu.system +
                        cpu.idle +
                        cpu.iowait +
                        cpu.irq +
                        cpu.softirq +
                        cpu.steal +
                        cpu.guest +
                        cpu.guest_nice
                }
                maps.set(name, cpu)
            }
        })
        return maps
    }
    async function hddStat(client) {
        const text = await client.execute(command.HDD)
        const time_ms = Date.now()
        const time = toTime(time_ms, 'mm:ss')
        const rows = text.split('\n')
        const maps = new Map()
        const cols = [
            'major',
            'minor',
            'name',
            'reads_completed_count',
            'reads_merged_count',
            'sectors_read',
            'ms_reading',
            'writes_completed_count',
            'writes_merged_count',
            'sectors_written',
            'ms_writing',
            'ios_in_progress',
            'ms_spent_doing_io',
            'ms_spent_doing_weighted_io'
        ]
        rows.forEach(it => {
            const disk = it
                .trim()
                .split(/\s+/)
                .reduce(
                    (initial, value, index) => {
                        let name = cols[index]
                        if (index == 2) {
                            initial[name] = value
                        } else {
                            initial[name] = Number(value)
                        }
                        return initial
                    },
                    {
                        time,
                        time_ms
                    }
                )
            disk.name_original = disk.name
            disk.name = 'hdd_' + disk.name
            maps.set(disk.name, disk)
        })
        return maps
    }
    async function netStat(client) {
        const text = await client.execute(command.NET)
        const time_ms = Date.now()
        const time = toTime(time_ms, 'mm:ss')
        const rows = text.split('\n')
        const maps = new Map()

        const [face, down, up] = rows.at(1).split('|')
        const downColumns = down.trim().split(/\s+/)
        const upColumns = up.trim().split(/\s+/)

        rows.slice(2).forEach(it => {
            let parts = it.trim().split(/\s+/)
            let name = 'net_' + parts.at(0).replace(':', '')
            let stat = parts.slice(1, downColumns.length).reduce(
                (initial, value, index) => {
                    let name = downColumns[index]
                    initial['down_' + name] = Number(value)
                    return initial
                },
                {
                    time_ms,
                    time,
                    name
                }
            )
            stat = parts.slice(1 + downColumns.length).reduce((initial, value, index) => {
                let name = upColumns[index]
                initial['up_' + name] = Number(value)
                return initial
            }, stat)
            maps.set(name, stat)
        })
        return maps
    }
    async function loadAvgStat(client) {
        const text = await client.execute(command.LOAD_AVG)
        const time_ms = Date.now()
        const time = toTime(time_ms, 'mm:ss')
        const maps = new Map()
        const [m01, m05, m10, run_total, pid] = text.trim().split(/\s+/)
        let run = 0
        let total = 0
        if (run_total) {
            let parts = run_total.split('/')
            if (parts.length > 0) {
                run = Number(parts.at(0))
            }
            if (parts.length > 1) {
                total = Number(parts.at(1))
            }
        }
        let stat = {
            name: 'load-avg_',
            time,
            m01: Number(m01),
            m05: Number(m05),
            m10: Number(m10),
            process_run: run,
            process_total: total,
            process_last_create_id: pid
        }
        maps.set(stat.name, stat)
        return maps
    }
    async function osStat(client) {
        const time_ms = Date.now()
        const time = toTime(time_ms, 'mm:ss')
        const text = await client.execute(command.OS)
        return text.split(/\n+/).reduce(
            (initial, value, index) => {
                let [k, v] = value.split('=')
                initial[k] = v
                return initial
            },
            {
                time,
                time_ms
            }
        )
    }
    async function uptimeStat(client) {
        const time_ms = Date.now()
        const time = toTime(time_ms, 'mm:ss')
        const text = await client.execute(command.UP_TIME)
        const [used, free] = text.trim().split(/\s+/)
        let upTotalTime
        let upTotalFreeTime
        if (used) {
            upTotalTime = toDuration(Number(used) * 1000, 'Y年M月D天H时m分')
        }
        if (free) {
            upTotalFreeTime = toDuration(Number(free) * 1000, 'Y年M月D天H时m分')
        }
        return {
            time,
            upTotalTime,
            upTotalFreeTime
        }
    }
    return {
        async mem(client) {
            const text = await client.execute(command.MEM)
            const time = now('mm:ss')
            const rows = text.split('\n')
            const cols = rows.at(0).split(/\s+/)
            const MEM = rows
                .at(1)
                .trim()
                .split(/\s+/)
                .slice(1)
                .reduce(
                    (initial, value, index) => {
                        let name = cols[index]
                        initial[name] = Number(value)
                        return initial
                    },
                    {
                        time,
                        name: 'mem_'
                    }
                )
            if (MEM.total > 0) {
                MEM.usedRatio = toRatio(MEM.used, MEM.total, 2)
                MEM.freeRatio = toRatio(MEM.free, MEM.total, 2)
            }
            let SWAP = rows
                .at(2)
                .trim()
                .split(/\s+/)
                .slice(1)
                .reduce(
                    (initial, value, index) => {
                        let name = cols[index]
                        initial[name] = Number(value)
                        return initial
                    },
                    {
                        time,
                        name: 'mem_swap'
                    }
                )
            if (SWAP.total > 0) {
                SWAP.usedRatio = toRatio(SWAP.used, SWAP.total, 2)
                SWAP.freeRatio = toRatio(SWAP.free, SWAP.total, 2)
            }
            return [MEM, SWAP]
        },
        async cpu(client) {
            let maps = await cpuStat(client)
            let list = []
            if (lastCPU) {
                maps.forEach((current, name) => {
                    const previous = lastCPU.get(name)
                    const item = {
                        name,
                        time: current.time,
                        user: current.user - previous.user,
                        nice: current.nice - previous.nice,
                        system: current.system - previous.system,
                        idle: current.idle - previous.idle,
                        iowait: current.iowait - previous.iowait,
                        irq: current.irq - previous.irq,
                        softirq: current.softirq - previous.softirq,
                        steal: current.steal - previous.steal,
                        guest: current.guest - previous.guest,
                        guest_nice: current.guest_nice - previous.guest_nice,
                        total: current.total - previous.total
                    }
                    let idle = (item.idle / item.total) * 100
                    let used = ((item.total - item.idle) / item.total) * 100
                    item.idleRatio = parseFloat(idle.toFixed(2))
                    item.usedRatio = parseFloat(used.toFixed(2))
                    list.push(item)
                })
            }
            lastCPU = maps
            return list
        },
        async hdd(client) {
            let list = []
            let maps = await hddStat(client)
            if (lastHDD) {
                for (const [name, current] of maps) {
                    const previous = lastHDD.get(name)
                    const item = {
                        name: name,
                        time: current.time,
                        reads_completed_count:
                            current.reads_completed_count - previous.reads_completed_count,
                        reads_merged_count:
                            current.reads_merged_count - previous.reads_merged_count,
                        sectors_read: current.sectors_read - previous.sectors_read,
                        ms_reading: current.ms_reading - previous.ms_reading,
                        writes_completed_count:
                            current.writes_completed_count - previous.writes_completed_count,
                        writes_merged_count:
                            current.writes_merged_count - previous.writes_merged_count,
                        sectors_written: current.sectors_written - previous.sectors_written,
                        ms_writing: current.ms_writing - previous.ms_writing,
                        ios_in_progress: current.ios_in_progress,
                        ms_spent_doing_io: current.ms_spent_doing_io - previous.ms_spent_doing_io,
                        ms_spent_doing_weighted_io:
                            current.ms_spent_doing_weighted_io -
                            previous.ms_spent_doing_weighted_io,
                        read_speed: 0,
                        write_speed: 0,
                        read_speed_mb: 0,
                        write_speed_mb: 0
                    }
                    let timespan = (current.time_ms - previous.time_ms) / 1000
                    let read_speed = item.sectors_read / timespan
                    let write_speed = item.sectors_written / timespan
                    item.read_speed = parseFloat(read_speed.toFixed(2))
                    item.write_speed = parseFloat(write_speed.toFixed(2))

                    let size = sectors.get(name)
                    if (isNil(size)) {
                        const command = 'blockdev --getbsz /dev/' + current.name_original
                        const value = await client.execute(command, {
                            encoding: 'utf-8'
                        })
                        size = parseInt(value.trim(), 10)
                        sectors.set(name, size)
                    }
                    item.read_speed_mb = (item.read_speed * size) / 1024 / 1024
                    item.write_speed_mb = (item.write_speed * size) / 1024 / 1024
                    list.push(item)
                }
            }
            lastHDD = maps
            return list
        },
        async net(client) {
            let list = []
            let maps = await netStat(client)
            if (lastNET) {
                maps.forEach((current, name) => {
                    const previous = lastNET.get(name)
                    const item = {
                        time: current.time,
                        time_ms: current.time_ms,
                        timespan: (current.time_ms - previous.time_ms) / 1000,
                        name: name,
                        down_bytes: current.down_bytes - previous.down_bytes,
                        down_packets: current.down_packets - previous.down_packets,
                        down_errs: current.down_errs - previous.down_errs,
                        down_drop: current.down_drop - previous.down_drop,
                        down_fifo: current.down_fifo - previous.down_fifo,
                        down_frame: current.down_frame - previous.down_frame,
                        down_compressed: current.down_compressed - previous.down_compressed,
                        up_bytes: current.up_bytes - previous.up_bytes,
                        up_packets: current.up_packets - previous.up_packets,
                        up_errs: current.up_errs - previous.up_errs,
                        up_drop: current.up_drop - previous.up_drop,
                        up_fifo: current.up_fifo - previous.up_fifo,
                        up_colls: current.up_colls - previous.up_colls,
                        up_carrier: current.up_carrier - previous.up_carrier,
                        up_compressed: current.up_compressed - previous.up_compressed,
                        down_speed: 0,
                        up_speed: 0,
                        down_packet_speed: 0,
                        up_packet_speed: 0
                    }
                    item.down_speed = toUnitValue(item.down_bytes / 1024, item.timespan, 2)
                    item.up_speed = toUnitValue(item.up_bytes / 1024, item.timespan, 2)
                    item.down_packet_speed = toUnitValue(item.down_packets, item.timespan, 2)
                    item.up_packet_speed = toUnitValue(item.up_packets, item.timespan, 2)
                    list.push(item)
                })
            }
            lastNET = maps
            return list
        },
        async process(client) {
            const map = {
                USER: 'user',
                PID: 'pid',
                '%CPU': 'cpu_usage',
                '%MEM': 'mem_usage',
                VSZ: 'vsz',
                RSS: 'rss',
                TTY: 'tty',
                STAT: 'stat',
                START: 'start_time',
                TIME: 'time',
                COMMAND: 'command'
            }
            const text = await client.execute(command.PROCESS)
            const time = now('mm:ss')
            const rows = text.split('\n')
            const cols = rows.at(0).trim().split(/\s+/)
            return rows.slice(1).map(it => {
                return it
                    .trim()
                    .split(/\s+/)
                    .reduce(
                        (initial, value, index) => {
                            let source = cols[index]
                            if (source) {
                                let target = map[source]
                                if (target) {
                                    initial[target] = value
                                } else {
                                    initial[source] = value
                                }
                            }
                            return initial
                        },
                        {
                            time
                        }
                    )
            })
        },
        async metrics(client) {
            let cpus = await cpuStat(client)
            let hdds = await hddStat(client)
            let nets = await netStat(client)
            let loadAvg = await loadAvgStat(client)
            return [
                {
                    id: 'cpu_',
                    children: [...cpus].map(([k, v]) => ({ id: k }))
                },
                {
                    id: 'mem_',
                    children: [
                        {
                            id: 'mem_'
                        },
                        {
                            id: 'mem_swap'
                        }
                    ]
                },
                {
                    id: 'hdd_',
                    children: [...hdds].map(([k, v]) => ({ id: k }))
                },
                {
                    id: 'net_',
                    children: [...nets].map(([k, v]) => ({ id: k }))
                },
                {
                    id: 'process_',
                    children: [
                        {
                            id: 'process_',
                            noChart: true
                        }
                    ]
                },
                {
                    id: 'user_',
                    children: [
                        {
                            id: 'user_',
                            noChart: true
                        }
                    ]
                },
                {
                    id: 'load-avg_',
                    children: [...loadAvg].map(([k, v]) => ({ id: k }))
                }
            ]
        },
        async loadAvg(client) {
            let maps = await loadAvgStat(client)
            return [...maps.values()]
        },
        async os(client) {
            return await osStat(client)
        },
        async uptime(client) {
            return await uptimeStat(client)
        },
        async users(client) {
            const cols = ['user', 'pwd', 'uid', 'gid', 'remark', 'home', 'shell']
            const text = await client.execute(command.USERS)
            const time = now('mm:ss')
            return text.split('\n').map(it => {
                return it
                    .trim()
                    .split(':')
                    .reduce(
                        (initial, value, index) => {
                            let k = cols[index]
                            if (k) {
                                initial[k] = value
                            }
                            return initial
                        },
                        { time }
                    )
            })
        }
    }
}
function changeChartInterval(value, cache = true) {
    if (chartTimer) {
        clearInterval(chartTimer)
        chartTimer = null
    }
    if (value > 0) {
        chartTimer = setInterval(() => {
            setALL.call(this)
        }, value * 1000)
    }
    chartInterval = value
    if (cache) {
        setting.set(chartIntervalCache, chartInterval)
    }
}
function changeProcessInterval(value, cache = true) {
    if (processTimer) {
        clearInterval(processTimer)
        processTimer = null
    }
    if (value > 0) {
        processTimer = setInterval(() => {
            setProcess.call(this)
        }, value * 1000)
    }
    processInterval = value
    if (cache) {
        setting.set(processIntervalCache, processInterval)
    }
}
function changeUserCharts(ids) {
    userCharts = [...new Set(ids)]
    return setting.set(userChartsCache, userCharts)
}
async function stopProcess(item) {
    let button = await hx.window.showMessageBox({
        type: 'question',
        title: '确定停止以下进程?',
        text: item.name,
        buttons: ['确定', '取消']
    })
    if (button == '确定') {
        let client = cache.connections.get(this.options.host).connection
        try {
            await client.execute('kill ' + item.pid)
            log.notice.info('关闭进程成功, 进程: %s', item.name)
        } catch (error) {
            log.notice.error('关闭进程失败, 进程: %s\n%s', item.name, error.message)
        }
    }
}
async function lockUser(item) {
    let button = await hx.window.showMessageBox({
        type: 'question',
        title: '确定锁定以下用户?',
        text: item.user,
        buttons: ['确定', '取消']
    })
    if (button == '确定') {
        let client = cache.connections.get(this.options.host).connection
        try {
            await client.execute('usermod -L ' + item.user)
            log.notice.info('锁定成功, 用户: %s', item.user)
        } catch (error) {
            log.notice.error('锁定失败, 用户: %s\n%s', item.user, error.message)
        }
    }
}
async function unlockUser(item) {
    let button = await hx.window.showMessageBox({
        type: 'question',
        title: '确定解锁以下用户?',
        text: item.user,
        buttons: ['确定', '取消']
    })
    if (button == '确定') {
        let client = cache.connections.get(this.options.host).connection
        try {
            await client.execute('usermod -U ' + item.user)
            log.notice.info('解锁成功, 用户: %s', item.user)
        } catch (error) {
            log.notice.error('解锁失败, 用户: %s\n%s', item.user, error.message)
        }
    }
}
module.exports = {
    onInit,
    changeChartInterval,
    changeProcessInterval,
    changeUserCharts,
    stopProcess,
    lockUser,
    unlockUser
}
